/**
 * TODO: Vložit licenci.
 */

package cz.enif.commander;

import java.util.HashMap;
import java.util.List;

import cz.enif.commander.sources.ISource;
import cz.enif.commander.values.IValue;

/**
 * Interpret skriptů.
 *
 * @author Přemysl Fára
 */
final class Interpreter implements IError
{
    //// CONSTRUCTORS ////

    /**
     * Konstruktor.
     */
    public Interpreter()
    {
    	this.parser = null;
		this.source = null;
		
		this.lastCalledCommandName = "";
		this.defaultCommand = new IUserCommand()
    	{
    		public boolean exec(List<IValue> params)
    		{
    			return cmdUndefined(params);
    		}
           
    		@Override
    		public String toString()
    		{
    			return "Unknown command";
    		}
    	};
		
		this.wasLastCommand = true;
    	
        this.commands = new HashMap<String, IUserCommand>(32);
        
        this.clearError();
    }

    //// PUBLIC ////
    
    /**
     * Připraví zdroj pro čtení parserem.
     * 
     * @param source Zdroj, který má být použit pro čtení scriptu.
     *   Nesmí být null. Pokud je již nějaký zdroj nastaven, je
     *   uzavřen.
     *   
     * @return True při úspěchu, jinak false.
     */
    public boolean setSource(ISource source)
    {
    	// Uzavřeme starý zdroj.
    	if (this.source != null)
    	{
    		this.parser = null;
    		this.source = null;
    	}
    	
    	// Nastavení nového zdroje znamená, 
    	// že se ve stávajícím zdroji se nastaví EOF.
    	this.wasLastCommand = true;
    	
    	// Pokud je nový nezadaný, chyba.
    	if (source == null) return false;
    	
    	// Nastavíme nový zdroj.
    	this.source = source;
    	this.parser = new Parser(new Scanner(source));
    	if (this.parser.wasError())
    	{
    		this.setError(this.parser.getErrorMessage());
    		
    		return false;
    	}
    	
    	// Máme zdroj, jsme na jeho začátku.
    	this.wasLastCommand = false;
    	
    	return true;
    }
    
    //// RUN AT ONCE ///
    
    /**
     * Přeloží celý skript.
     * 
     * Zdroj musí být zadán přez setSource(source).
     * 
     * @return True při úspěchu, jinak false.
     */
    public List<Command> compile()
    {
    	if (this.source == null)
    	{
    		this.setError("Není zadán zdroj.");
    		
    		return null;
    	}
    	
    	// Přeložíme script na příkazy.
    	List<Command> commands = this.parser.parse();
        
        // V případě chyby jí předáme dál.
        if (this.parser.wasError())
        {
        	this.setError(this.parser.getErrorMessage());
        	
            return null;
        }

        // Uzavřeme zpracovaný zdroj a uvolníme paměť parseru.
    	// Pozn.: Zruší se parser i source. setSource() vrátí false, 
        // protože jsme mu nezadali zdroj, ale to je v tomto 
        // případě v pořádku.
    	this.setSource(null);
        
        // Spustíme přeložené příkazy.
        return commands;
    }
    
    /**
     * Spustí přeložený script.
     *
     *  @param comands Seznam instrukcí programu, který se má provést.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean run(List<Command> commands)
    {
        if (commands == null)
        {
        	// nelze zpracovat prázdný kód
        	this.setError("Nebyl zadán kód ke zpracování.");
        	
            return false;
        }

        for (Command c : commands)
        {
            // příkaz
            IUserCommand command = this.getCommand(c.getName());
            if (command == null) return false;
               
            // spuštění příkazu
            boolean status = command.exec(c.getParams());
            if (status == false) return false;
        }

        return true;
    }
    
    //// RUN STEB BY STEP ////
    
    /**
     * Provede jeden příkaz ze zdroje. Zdroj musí být nastaven
     * pomocí setSource().
     * 
     * @return True při úspěchu, jinak false.
     */
    public boolean runCommand()
    {
    	if (this.source == null)
    	{
    		this.setError("Není zadán zdroj.");
    		
    		return false;
    	}
    	
    	Command c = this.parser.parseCommand();
    	
    	// V případě chyby jí předáme dál.
        if (parser.wasError())
        {
        	this.setError(parser.getErrorMessage());
        	
            return false;
        }
        
        // už nejsou žádné další příkazy a nedošlo k žádné chybě
        if (c == null)
        {
        	// nastaví wasEOF na true
        	this.setSource(null);
        	
        	return true;
        }
    	
        // příkaz
        IUserCommand command = this.getCommand(c.getName());
        if (command == null) return false;
           
        // spuštění příkazu
        return command.exec(c.getParams());
    }

    //// STATE ////
    
    /**
     * Vrací true, pokud byl přečten a zpracován poslední příkaz.
     * 
     * @return True, pokud byl přečten a zpracován poslední příkaz.
     */
    public boolean wasLastCommand()
    {
    	return this.wasLastCommand;
    }
    
    //// ERRORS ////
    
    /* (non-Javadoc)
	 * @see cz.enif.commander.IError#wasError()
	 */
	@Override
	public boolean wasError() 
	{
		return this.wasError;
	}

	/* (non-Javadoc)
	 * @see cz.enif.commander.IError#getErrorMessage()
	 */
	@Override
	public String getErrorMessage() 
	{
		return this.errorMessage;
	}
    
    //// COMMANDS ////

    /**
     * Definuje nový příkaz se jménem commandName, případně změní již definovanému
     * příkazu jeho implementaci.
     *
     * @param commandName Jméno nově definovaného příkazu. Povinný parametr.
     * @param command Odkaz na implementaci příkazu. Povinný parametr.
     *
     * @return True při ůspěchu, jinak false.
     */
    public boolean addCommand(String commandName, IUserCommand command)
    {
        if (commandName == null || commandName.isEmpty())
        {
        	this.setError("Nezadán název příkazu.");

            return false;
        }

        if (command == null)
        {
        	this.setError("Nezadán příkaz s názvem '" + commandName + "'.");

            return false;
        }
        
        this.commands.put(commandName, command);

        return true;
    }

    /**
     * Zjistí, zda je definován příkaz se zadaným jménem.
     *
     * @param commandName Jméno příkazu.
     *
     * @return True, pokud příkaz se zadaným jménem existuje, jinak false.
     */
    public boolean haveCommand(String commandName)
    {
        if (commandName == null || commandName.isEmpty())
        {
            return false;
        }

        return this.commands.containsKey(commandName);
    }

    /**
     * Vrací příkaz se jménem commandName, nebo null, pokud není zadáno commandName,
     * či neexistuje příkaz se zadaným jménem.
     *
     * @param commandName Jméno příkazu, který chceme získat.
     *
     * @return Odkaz na třídu implementující příkaz zadaného jména, nebo null,
     *   pokud takový příkaz neexistuje.
     */
    public IUserCommand getCommand(String commandName)
    {
        if (commandName == null || commandName.isEmpty())
        {
        	this.lastCalledCommandName = "";
        	
            this.setError("Nezadán název příkazu.");

            return null;
        }

        // Zapamatujeme si jméno posledně volaného příkazu.
        this.lastCalledCommandName = commandName;
        
        if (this.haveCommand(commandName))
        {
            return this.commands.get(commandName);
        }
        else
        {
        	if (this.defaultCommand != null)
        	{
        		return this.defaultCommand;
        	}

        	this.setError("Nedefinovaný příkaz - '" + commandName + "'.");
        		
        	return null;
        }
    }
    
    /**
     * Vrací defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     * 
     * @return Defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     */
    public IUserCommand getDefaultCommand()
    {
    	return this.defaultCommand;
    }
    
    /**
     * Nastaví defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     * 
     * @param DefaultCommand defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     */
    public void setDefaultCommand(IUserCommand defaultCommand)
    {
    	if (defaultCommand != null)
    	{
    		this.defaultCommand = defaultCommand;
    	}
    }
    
    /**
     * Vrací jméno posledně volaného, resp. právě běžícího, příkazu. 
     * 
     * @return Jméno posledně volaného, resp. právě běžícího, příkazu.
     */
    public String getLastCalledCommandName()
    {
    	return this.lastCalledCommandName;
    }
    
    //// PRIVATE ////

    /** Definované příkazy. */
    private HashMap<String, IUserCommand> commands;
    
    private ISource source;
    private Parser parser;
    
    private IUserCommand defaultCommand;
    private String lastCalledCommandName; 
    
    private boolean wasLastCommand;
    
    private boolean wasError;
    private String errorMessage;

    /**
     * Defaultní příkaz volaný pro nedefinované příkazy.
     */
    private boolean cmdUndefined(List<IValue> params)
    {
   		this.setError("Vyvolán nedefinovaný příkaz '" + this.lastCalledCommandName + "'.");

    	return false;
    }
    
    /**
     * Vymaže popis chyby.
     */
    private void clearError()
    {
    	this.wasError = false;
    	this.errorMessage = "OK";
    }
    
    /**
     * Nastaví příznak chyby a její popis.
     * 
     * @param message Popis chyby.
     */
    private void setError(String message)
    {
    	this.wasError = true;
    	this.errorMessage = (message == null ? "ERROR" : message);
    }
}
